import DateFormatConfig from "../../config/date-format-config";
import CommonService from "../../services/common-service";
import DynamicElementDataFactory from "../dynamic-element-data-factory";
import DynamicElementDefinition from "../dynamic-element-definition";
import DynamicElementDefinitionConstant from "../dynamic-element-definition-constant";
import ChartTypeConstant from "./chart-type-constant";
import EchartElementDataFieldMapping from "./echart-element-data-field-mapping";

export default class EChartElementOptionFactory {
  /**
   * Generates echart option based on its defintion.
   * @param definition The configured child node which includes all configurations.
   */
  static async generateOptions(definition: DynamicElementDefinition): Promise<any> {
    let option: any = {};

    const indexList = definition.config.indexes.map((p: any) => {
      return {
        value: p.IDX_ID,
        name: p.INDEX_NAME,
      }
    });

    // Sets custmoized color option.
    this.setColorOption(option);

    // Queries data from api.
    const data = (await DynamicElementDataFactory.queryData(definition)).data;

    if (definition.config.optionCfg.type === ChartTypeConstant.map) {
      // Sets dataset option for map chart.
      this.setMapChartDatasetOption(option, definition.type, data, indexList);

      // Sets specific option for map chart.
      this.setMapOption(option, definition.config.optionCfg);
    } else {
      // Sets dataset option for general chart.
      this.setGeneralChartDatasetOption(option, definition.type, data, indexList);

      // Sets title and legend option.
      this.setTitleOption(option, definition.config.optionCfg);

      // Sets different option based on chart type.
      if (definition.config.optionCfg.type === ChartTypeConstant.line || definition.config.optionCfg.type === ChartTypeConstant.bar) {
        // Sets x & y axis option.
        this.setAxisOption(option);

        // Sets series option.
        this.setSeriesOption(option, definition.config);
      } else if (definition.config.optionCfg.type === ChartTypeConstant.pie) {
        // Sets pie option.
        this.setPieOption(option, definition.config);
      }
    }

    return option;
  }

  /**
   * Prepares generaal dataset property for echart option. NOT support map chart.
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   * @param data Data array for chart.
   * @param indexList The index info in config. Value maps to IDX_ID. Name maps to INDEX_NAME.
   * Example:
   * [
   *   {value: 2115030856, name: "纽约/NewYork"}
   *   {value: 2115030857, name: "亨利港/Henry Hub"}
   *   {value: 2115030858, name: "德克萨斯/Texas"}
   *   {value: 2115030859, name: "埃科/AECO Canada"},
   * ]
   */
  private static setGeneralChartDatasetOption(chartOption: any, type: string, data: any[], indexList: any[]): void {
    const sources: any[] = [];
    const dimensions: any[] = [];
    let indexMapping: any = {};
    let source: any = {};

    // Parses index list to object.
    //
    // Example:
    // { 2115030856: "纽约/NewYork", 2115030857: "亨利港/Henry Hub", 2115030858: "德克萨斯/Texas", 2115030859: "埃科/AECO Canada" }
    for (const item of indexList) {
      indexMapping[item.value] = item.name;
    }

    // Gets the mapping looks like this:
    // {
    //   indexID: 'IDX_ID',
    //   indexName: 'INDEX_NAME',
    //   xAxis: 'PUBLISH_DT',
    //   yAxis: 'INDEX_VAL',
    // }
    const dataFieldMapping = EchartElementDataFieldMapping.getMapping(type);

    for (const item of data) {
      const idxValue = item[dataFieldMapping.indexID];
      const dimension = indexMapping[idxValue];
      const xValue = item[dataFieldMapping.xAxis];
      const yValue = item[dataFieldMapping.yAxis];

      const sourceItem = source[xValue] ?? {};
      sourceItem.header = CommonService.formatDate(new Date(xValue), DateFormatConfig.shortDateFormat);
      sourceItem[dimension] = yValue;

      source[xValue] = sourceItem;
    }

    const keys = Object.keys(source).sort();

    for (const key of keys) {
      let sourceObj: any = {};

      Object.entries(source[key]).forEach(([k, v]) => {
        sourceObj[k] = v;
      });

      sources.push(sourceObj);
    }

    dimensions.push('header');

    for (const item of indexList) {
      dimensions.push(item.name);
    }

    chartOption.dataset = [
      {
        source: sources,
        dimensions: dimensions,
      },
    ];
  }

  /**
   * Prepares specific dataset property for map chart option. NOT for general chart.
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   * @param data Data array for chart.
   * @param indexList The index info in config. Value maps to IDX_ID. Name maps to INDEX_NAME.
   * Example:
   * [
   *   {value: 2115030856, name: "纽约/NewYork"}
   *   {value: 2115030857, name: "亨利港/Henry Hub"}
   *   {value: 2115030858, name: "德克萨斯/Texas"}
   *   {value: 2115030859, name: "埃科/AECO Canada"},
   * ]
   */
  private static setMapChartDatasetOption(chartOption: any, type: string, data: any[], indexList: any[]): void {
    const sources: any[] = [];
    const dimensions: any[] = [];
    let indexMapping: any = {};

    // Parses index list to object.
    //
    // Example:
    // { 2115030856: "纽约/NewYork", 2115030857: "亨利港/Henry Hub", 2115030858: "德克萨斯/Texas", 2115030859: "埃科/AECO Canada" }
    for (const item of indexList) {
      indexMapping[item.value] = item.name;
    }

    // Gets the mapping looks like this:
    // {
    //   indexID: 'IDX_ID',
    //   indexName: 'INDEX_NAME',
    //   xAxis: 'PUBLISH_DT',
    //   yAxis: 'INDEX_VAL',
    // }
    const dataFieldMapping = EchartElementDataFieldMapping.getMapping(type);

    data.forEach((item) => {
      const idxId = parseInt(item[dataFieldMapping.indexID]);
      const idxValue = item[dataFieldMapping.indexValue];

      if (!CommonService.isNullOrUndefined(idxValue)) {
        let dValue: number = parseFloat(idxValue);

        if (dValue) {
          sources.push({
            header: indexMapping[idxId],
            value: dValue,
          });
        }
      }
    });

    dimensions.push('header');
    dimensions.push('value');

    chartOption.dataset = [
      {
        source: sources,
        dimensions: dimensions,
      },
    ];
  }

  /**
 * Gets option for map chart.
 * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
 * @param titleOptionCfg The chart title option config from optionCfg property.
 */
  private static setMapOption(chartOption: any, titleOptionCfg?: any | null): void {
    const inRange: any[] = [];

    if (!CommonService.isNullOrUndefined(titleOptionCfg[DynamicElementDefinitionConstant.mapCfgWidget])) {
      const dataCfg: any = titleOptionCfg[DynamicElementDefinitionConstant.mapCfgWidget];
      const isCustomColor: boolean = dataCfg.isCustomColor;

      if (isCustomColor) {
        inRange.push(...[
          dataCfg.color1,
          dataCfg.color2,
          dataCfg.color3,
        ]);
      } else {
        inRange.push(...[
          'lightskyblue',
          'yellow',
          'orangered',
        ]);
      }
    } else {
      inRange.push(...[
        'lightskyblue',
        'yellow',
        'orangered',
      ]);
    }

    let min: number = 0;
    let max: number = 100;

    const datasets: any[] = chartOption.dataset;

    if (datasets?.length > 0) {
      const dataset: any = datasets[0];
      const dimensions: any[] = dataset.dimensions;

      if (dimensions?.length > 1) {
        const key: string = dimensions[1];
        const source: any[] = dataset.source;

        let i: number = 0;

        for (const item of source) {
          const value = item[key];

          if (!CommonService.isNullOrUndefined(value)) {
            const dValue = parseFloat(value);

            if (i++ == 0) {
              max = dValue;
              min = dValue;
            } else {
              if (max < dValue) {
                max = dValue;
              }

              if (min > dValue) {
                min = dValue;
              }
            }
          }
        }
      }
    }

    chartOption.tooltip = {
      backgroundColor: 'rgba(255,255,255)',
      trigger: 'item',
      formatter: '{b}<br/>{c}',
    };

    chartOption.toolbox = {
      feature: {
        dataView: {
          readOnly: true,
        },
        saveAsImage: {},
      },
      show: true,
    };

    chartOption.visualMap = {
      min: min,
      max: max,
      realtime: false,
      calculable: true,
      text: [
        '高',
        '低',
      ],
      inRange: {
        color: inRange,
      },
    };

    chartOption.series = {
      type: 'map',
      map: 'CN',    // This MUST match first param of echarts.registerMap method.
      label: {
        show: false,
      },
    };
  }

  /**
   * Sets customized color option.
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   */
  private static setColorOption(chartOption: any): void {
    chartOption.color = [
      "#7CB5EC",
      "#F7A35C",
      "#8085E9",
      "#F15C80",
      "#E4D354",
      "#2B908F",
      "#F45B5B",
      "#91E8E1",
      "#058DC7",
      "#50B432",
      "#ED561B",
      "#DDDF00",
      "#24CBE5",
      "#64E572",
      "#FF9655",
      "#FFF263",
      "#6AF9C4",
      "#D47F00",
      "#00FFFF",
      "#D4FF55",
      "#4572A7",
      "#AA4643",
      "#89A54E",
      "#80699B",
      "#3D96AE",
      "#DB843D",
      "#92A8CD",
      "#A47D7C",
      "#7FBF55",
      "#A5C2D5",
      "#CBAB4F",
      "#76A871",
      "#A56F8F",
      "#C12C44",
      "#9F7961",
      "#76A871",
      "#6F83A5",
      "#0F4FB8",
      "#106DCF",
      "#B3D74C",
      "#74AAE3",
      "#5CDEC6",
      "#3526DE",
      "#9D65EE",
      "#A8B3E3",
      "#6BC1B7",
      "#549EE2",
      "#6E98D6",
    ];
  }

  /**
   * Sets xAxis & yAxis in chart option.
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   */
  private static setAxisOption(chartOption: any): void {
    chartOption.xAxis = {
      type: 'category',
    };

    chartOption.yAxis = [
      {},
      {},
    ];
  }

  /**
   * Gets option for chart title and legend.
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   * @param titleOptionCfg The chart title option config from optionCfg property.
   */
  private static setTitleOption(chartOption: any, titleOptionCfg?: any | null): void {
    if (!CommonService.isNullOrUndefined(titleOptionCfg[DynamicElementDefinitionConstant.titleOption])) {
      const dataCfg: any = titleOptionCfg[DynamicElementDefinitionConstant.titleOption];
      delete chartOption.title;

      if (dataCfg.showLegend) {
        const legend: any = {
          type: 'scroll',
        };

        if (dataCfg.legendPosition?.toUpperCase() === 'H') {
          legend.orient = 'horizontal';
        } else {
          legend.orient = 'vertical';
        }

        if (dataCfg.legendHPosition?.toUpperCase() === 'C') {
          legend.x = 'center';
        } else if (dataCfg.legendHPosition?.toUpperCase() === 'L') {
          legend.x = 'left';
        } else {
          legend.x = 'right';
        }

        if (dataCfg.legendVPosition?.toUpperCase() === 'T') {
          legend.y = 'top';
        } else if (dataCfg.legendVPosition?.toUpperCase() === 'B') {
          legend.y = 'bottom';
        } else {
          legend.y = 'center';
        }

        chartOption.legend = legend;
      } else {
        delete chartOption.legend;
      }
    } else {
      delete chartOption.title;
      delete chartOption.legend;
    }

    chartOption.tooltip = {
      backgroundColor: 'rgba(255,255,255)',
      trigger: 'axis',
      confine: true,
    };

    chartOption.toolbox = {
      show: true,
      feature: {
        dataView: {
          readOnly: true,
        },
        saveAsImage: {},
      },
    };
  }

  /**
   * Gets option for chart series.
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   * @param config The chart entire config.
   */
  private static setSeriesOption(chartOption: any, config?: any | null): void {
    const datasets: any[] = chartOption.dataset;
    const series: any[] = [];

    if (datasets?.length > 0) {
      const dataset: any = datasets[0];
      const dimensions: any[] = dataset.dimensions;
      const configSeries = config.optionCfg[DynamicElementDefinitionConstant.seriesOptionCfg];

      // Skips first item.
      for (let i = 1; i < dimensions.length; i++) {
        if (!CommonService.isNullOrUndefined(configSeries[dimensions[i]])) {
          const item = configSeries[dimensions[i]];
          const type = parseInt(item.type);
          let seriesItem: any = { name: dimensions[i] };

          if (type === ChartTypeConstant.line) {
            seriesItem.type = 'line';
          } else if (type === ChartTypeConstant.area) {
            seriesItem.type = 'line';
            seriesItem.areaStyle = {};
          } else {
            seriesItem.type = 'bar';
          }

          seriesItem.showSymbol = false;
          const color = item.color;

          if (!CommonService.isNullOrUndefined(color)) {
            seriesItem.color = [color];
          }

          if (!CommonService.isNullOrUndefined(item.yAxisIndex)) {
            seriesItem.yAxisIndex = item.yAxisIndex;
          }

          seriesItem.connectNulls = true;
          series.push(seriesItem);
        } else {
          series.push({
            name: dimensions[i],
            type: 'line',
            connectNulls: true,
            showSymbol: false,
          });
        }
      }
    }

    chartOption.series = series;
  }

  /**
   * 
   * @param chartOption Input chart option. This param will be updated inside this method. Please treat this param as an output param.
   */
  private static setPieOption(chartOption: any, config?: any | null): void {
    const transposed: boolean = config.optionCfg[DynamicElementDefinitionConstant.pieOptionCfg].transposed ?? false;

    if (transposed) {
      const datasets: any[] = chartOption.dataset;

      if (datasets.length === 1) {
        const dataset: any = datasets[0];
        const dimensions: any[] = dataset.dimensions;
        const sources: any[] = dataset.source;
        const tDimensions: any[] = ['header'];
        const tSourceMap: any = {};

        for (const item of sources) {
          const header = item.header;
          tDimensions.push(header);

          // Skips first dimension.
          for (let i = 1; i < dimensions.length; i++) {
            const dimension = dimensions[i];
            const tSourceMapItem = tSourceMap[dimension] ?? {};

            tSourceMapItem[header] = item[dimension];
            tSourceMapItem.header = dimension;
            tSourceMap[dimension] = tSourceMapItem;
          }
        }

        const tSource: any[] = [];

        Object.entries(tSourceMap).forEach(([_, value]) => {
          const sourceObj: any = {};

          Object.entries((value as any)).forEach(([k, v]) => {
            sourceObj[k] = v;
          });

          tSource.push(sourceObj);
        });

        datasets.push({
          dimensions: tDimensions,
          source: tSource,
        });

        chartOption.dataset = datasets;
      }
    }

    const series: any[] = [];

    const seriesItem: any = {
      type: 'pie',
      datasetIndex: transposed ? 1 : 0,
    };

    const encode: any = {
      itemName: 'header',
    };

    const dataset: any = chartOption.dataset[transposed ? 1 : 0];
    encode.value = dataset.dimensions[1];
    seriesItem.encode = encode;
    series.push(seriesItem);
    chartOption.series = series;

    delete chartOption.xAxis;
    delete chartOption.yAxis;
  }
}